import sqlite3 from 'sqlite3';
import { isFunction, isPlainObject } from './is.js';
import { touch } from './fs.js';
import { EventEmitter } from 'events';

const { Database, verbose } = sqlite3;

class SqlWrapper {
    constructor(sql) {
        this._sql = (sql || '').trim().toLowerCase();
    }

    get sql() {
        return this._sql;
    }

    get isSelect() {
        return this._sql.indexOf('select') === 0;
    }

    formatParams(params) {
        if (params && isPlainObject(params)) {
            let r = {};
            Reflect.ownKeys(params).forEach(key => {
                r[`$${key}`] = params[key];
            });
            return r;
        }
        return params;
    }
}

const utils = {
    async getDB(path, { mode, trace, profile, busyTimeout }) {
        await touch(path);
        const db = await new Promise((resolve, reject) => {
            if (mode) {
                const db = new Database(path, mode, error => error ? reject(error) : resolve(db));
            } else {
                const db = new Database(path, error => error ? reject(error) : resolve(db));
            }
        });
        if (busyTimeout) {
            db.configure('busyTimeout', busyTimeout);
        }
        if (trace) {
            db.on('trace', (...args) => trace(db, ...args));
        }
        if (profile) {
            db.on('profile', (...args) => profile(db, ...args));
        }
        return db;
    }
}

class ConnectionPool {
    constructor({ path, max = 5, min = 2, trace, mode, profile, busyTimeout } = {}) {
        this._path = path;
        this._max = max;
        this._min = min;
        this._current = 0;
        this._option = { trace, mode, profile, busyTimeout };
        this._connections = [];
    }

    async getConnection() {
        let r = this._connections.find(a => a.idle);
        if (r) {
            return r;
        }
        let connection = null;
        if (this._current <= this._max) {
            this._current++;
            connection = await this._newConnection(this._path, this._option);
            this._connections.push(connection);
        } else {
            try {
                connection = await this._getIdleConnection();
            } catch (_) {
                connection = await this._newConnection(this._path, this._option);
                this._connections.push(connection);
            }
        }
        connection._idle = false;
        return connection;
    }

    async _newConnection(path, option) {
        const db = await utils.getDB(path, option);
        const connection = new Connection(this._connections.length, db);
        connection.on('idle', () => {
            this._connections = this._connections.filter(a => !a.isClose);
            if (this._connections.length > this._max) {
                this._connections.filter(a => a.idle).splice(0, this._max - this._connections.length).forEach(a => a.close());
            }
        });
        connection.on('close', con => {
            this._connections.splice(this._connections.indexOf(con), 1);
        });
        return connection;
    }

    async _getIdleConnection() {
        return new Promise((resolve, reject) => {
            let count = 0;
            let check = () => {
                let t = this._connections.find(a => a.idle);
                if (t) {
                    resolve(t);
                } else {
                    count++;
                    if (count >= 200) {
                        reject();
                    }
                    setTimeout(() => check(), 50);
                }
            }
            check();
        });
    }
}

class Connection extends EventEmitter {
    constructor(index, db) {
        super();
        db.index = index;
        this._db = db;
        this._index = index;
        this._idle = true;
        this._close = false;
    }

    get db() {
        return this._db;
    }

    get idle() {
        return !this._close && this._idle;
    }

    get isClose() {
        return this._close;
    }

    query(sql, params) {
        let wrapper = new SqlWrapper(sql);
        return new Promise((resolve, reject) => {
            if (wrapper.isSelect) {
                this.db.all(wrapper.sql, wrapper.formatParams(params), (error, rows) => {
                    error ? reject(error) : resolve(rows);
                });
            } else {
                this.db.run(wrapper.sql, wrapper.formatParams(params), function (error) {
                    error ? reject(error) : resolve({ changes: this.changes, lastID: this.lastID });
                });
            }
        });
    }

    runBatch(sql, paramList) {
        let wrapper = new SqlWrapper(sql);
        return new Promise((resolve, reject) => {
            const stmt = db.prepare(wrapper.sql, error => {
                error ? reject(error) : resolve(stmt);
            });
        }).then(stmt => {
            let r = [];
            return Promise.all((paramList || []).map(a => {
                return new Promise(resolve => {
                    stmt.run(i, wrapper.formatParams(a), function (error) {
                        r.push({ error, params: a, changes: this.changes, lastID: this.lastID });
                        resolve();
                    });
                });
            })).then(() => r);
        });
    }

    each(sql, params, fn) {
        let wrapper = new SqlWrapper(sql);
        return new Promise((resolve, reject) => {
            this.db.all(wrapper.sql, wrapper.formatParams(params), (_, row) => fn && fn(row), (error) => error ? reject(error) : resolve());
        });
    }

    getOne(sql, params) {
        let wrapper = new SqlWrapper(sql);
        return new Promise((resolve, reject) => {
            this.db.all(wrapper.sql, wrapper.formatParams(params), (error, row) => {
                error ? reject(error) : resolve(row);
            });
        });
    }

    close() {
        return new Promise((resolve, reject) => {
            this.db.close(error => {
                if (error) {
                    reject(error);
                } else {
                    this._close = true;
                    this.emit('close', this);
                    resolve();
                }
            });
        });
    }

    release() {
        this._idle = true;
        console.log('connection:[', this._index, '] release');
        this.emit('idle', this);
    }

    beginTransaction() {
        console.log('connection:[', this._index, '] begin transaction');
        return this.query(`BEGIN TRANSACTION`);
    }

    commitTransaction() {
        console.log('connection:[', this._index, '] commit transaction');
        return this.query(`COMMIT TRANSACTION`);
    }

    rollbackTransaction() {
        console.log('connection:[', this._index, '] rollback transaction');
        return this.query(`ROLLBACK TRANSACTION`);
    }
}

const DataSourceMap = new Map();

export const DataSource = {
    verbose() {
        verbose();
        return this;
    },
    register(key, option = {}) {
        DataSourceMap.set(key, new ConnectionPool(option));
        return this;
    },
    async getConnection(key) {
        if (DataSourceMap.get(key)) {
            return DataSourceMap.get(key).getConnection();
        }
        return null;
    },
    getService(service, { dataSource, transaction = [], before, after } = {}, ...args) {
        if (!dataSource) {
            throw Error(`dataSource not register`);
        }
        if (typeof service === 'function') {
            service = new service(...args);
        }
        return new Proxy(service, {
            get(service, name) {
                if (typeof service[name] === 'function') {
                    return async (...args) => {
                        const connection = await DataSourceMap.get(dataSource).getConnection();
                        let isTransaction = false;
                        if (transaction && Array.isArray(transaction)) {
                            isTransaction = transaction.includes(name);
                        }
                        if (transaction && isFunction(transaction)) {
                            isTransaction = transaction(name);
                        }
                        const context = {
                            connection,
                            isTransaction,
                            args,
                            data: null,
                            error: null
                        };
                        return Promise.resolve().then(() => {
                            if (isTransaction) {
                                return connection.beginTransaction();
                            }
                        }).then(() => {
                            if (before) {
                                return before(name, context);
                            }
                            return args;
                        }).then((args = []) => {
                            context.args = args;
                            return service[name](connection, ...(args || []));
                        }).then(data => {
                            if (isTransaction) {
                                return connection.commitTransaction().then(() => data);
                            }
                            return data;
                        }).then(data => {
                            if (after) {
                                context.data = data;
                                return Promise.resolve().then(() => after(name, context));
                            }
                            return data;
                        }).catch(e => {
                            return Promise.resolve().then(() => {
                                return connection.rollbackTransaction();
                            }).then(() => {
                                if (after) {
                                    context.error = e;
                                    return after(name, context);
                                }
                                return Promise.reject(e);
                            });
                        }).finally(() => {
                            return connection.release();
                        });
                    }
                }
                return service[name];
            }
        });
    }
}